import {RecordCategory, RecordType} from "./record.js";
import {toRaw} from "vue";

export class RecordAndPoints {
    constructor() {
        this.recordsToPoints = new Map();
        this.pointsToRecord = new Map();
    }
    add(rec, point) {
        if (this.recordsToPoints.has(rec)) {
            this.recordsToPoints.get(rec).push(point);
        } else {
            this.recordsToPoints.set(rec, [point]);
        }
        this.pointsToRecord.set(point, rec);
        return point
    }
    addPoint(point) {
        return this.add(null, point);
    }
    getPoints(rec) {
        return this.recordsToPoints.get(rec) || [];
    }
    merge(rap) {
        this.pointsToRecord = new Map([...this.pointsToRecord, ...rap.pointsToRecord]);
        rap.recordsToPoints.forEach((value, key) => {
            this.recordsToPoints.set(key, [
                ...(this.recordsToPoints.get(key) || []),
                ...value
            ]);
        });
    }
}

export class NamerForRecordType {
    constructor(records) {
        this.recordTypesToNum = this.#summaryRecordsNumByType(records);
        this.recordTypesToSeenNum = new Map();
    }

    nameType(recordType) {
        if ((this.recordTypesToNum.get(recordType) || 0) === 1) {
            return recordType.desc;
        }
        this.recordTypesToSeenNum.set(recordType, (this.recordTypesToSeenNum.get(recordType) || 0) + 1);
        return recordType.desc + '-' + this.recordTypesToSeenNum.get(recordType);
    }

    #summaryRecordsNumByType(records) {
        let recordTypesToNum = new Map();
        for (let rec of records) {
            recordTypesToNum.set(rec.type, (recordTypesToNum.get(rec.type) || 0) + 1);
        }
        return recordTypesToNum;
    }
}

class AxisCalculator {
    constructor(targetSize, start, end) {
        this.start = start;
        this.range = end - start;
        this.target = targetSize;
    }
    calc(value) {
        let relative = value - this.start;
        return Math.floor(this.target * relative / this.range);
    }
}


class YAxisCalculator extends AxisCalculator {
    constructor(yHeight, streamNum) {
        super(yHeight, 0, streamNum);
    }
}

export class StreamLineRender {
    constructor(streamId, startTs, endTs, yAxis, xWidth, records, pointNamer) {
        this.streamId = streamId;
        this.startTs = startTs;
        this.endTs = endTs;
        this.yAxis = yAxis;
        this.records = records;
        this.pointNamer = pointNamer;
        this.xCalc = new AxisCalculator(xWidth, startTs, endTs);
    }

    calcLineSegments(records) {
        const statesToSwitches = {
            within: [RecordType.localRecycle, RecordType.recycle, RecordType.birthRecycle],
            without: [RecordType.allocMem, RecordType.wander]
        };

        let rap /* records and points */ = new RecordAndPoints();
        let segments = [];
        let current = {
            point: rap.addPoint({
                name: "s" + this.streamId + "s",
                timestamp: this.startTs
            }),
            state: "without",  // within/without life cycle
            pos: -1
        };

        let render = this;
        const addSegments = function (index, endPoint) {
            segments.push({
                start: current.point,
                end: endPoint,
                state: current.state
            });
            current.state = current.state === "within" ? "without" : "within";
            current.pos = index;
            current.point = endPoint;
        }
        const findNext = function () {
            for (let i = current.pos + 1; i < records.length; ++i) {
                let rec = records[i];
                if (rec.stream === render.streamId) {
                    if (statesToSwitches[current.state].includes(rec.type)) {
                        addSegments(i, rap.add(rec, {
                            name: render.pointNamer.nameType(rec.type),
                            timestamp: rec.timestamp
                        }));
                        return;
                    }
                }
            }
            addSegments(records.length, rap.addPoint({
                name: "s" + render.streamId + "e",
                timestamp: render.endTs
            }));
        };

        while (current.pos < records.length) {
            findNext();
        }

        return {
            segments: segments,
            rap: rap
        };
    }

    draw() {
        let segInfo = this.calcLineSegments(this.records);
        let series = {
            data: [],
            links: []
        };
        let first = true;
        for (let seg of segInfo.segments) {
            if (first) {
                series.data.push({
                    name: seg.start.name,
                    x: this.xCalc.calc(seg.start.timestamp),
                    y: this.yAxis,
                    symbol: 'none'
                });
            }
            series.data.push({
                name: seg.end.name,
                x: this.xCalc.calc(seg.end.timestamp),
                y: this.yAxis,
                symbol: 'none'
            });
            let link = {
                source: seg.start.name,
                target: seg.end.name,
                lineStyle: {
                    color: seg.state === "within" ? "#33CC33" : "#CCCCCC"
                }
            };
            if (first) {
                link.label = {
                    show: true,
                    position: "start",
                    formatter: () => "stream " + this.streamId + ": "
                };
                first = false;
            }
            series.links.push(link);
        }
        return {
            rap: segInfo.rap,
            series: series
        };
    }
}

function isEventRecord(rec) {
    return rec.category === RecordCategory.Event
        || rec.type === RecordType.recycleSendEvent
        || rec.type === RecordType.recycleWaitEvent
        || rec.type === RecordType.migrationSendEvent
        || rec.type === RecordType.migrationWaitEvent;
}

function dropGeneralEventRecord(records) {
    let ret = [];
    for (let rec of records) {
        if (rec.category === RecordCategory.Event) {
            continue;
        }
        ret.push(rec);
    }
    return ret;
}

class EventsLineRender {
    constructor(records, xCalc, yCalc) {
        this.records = records;
        this.xCalc = xCalc;
        this.yCacl = yCalc;
    }
    draw() {
        let eventIdsToRecords = new Map();
        for (let rec of this.records) {
            if (!isEventRecord(rec)) {
                continue;
            }
            let eventRecords = eventIdsToRecords.get(rec.eventId);
            if (eventRecords === undefined) {
                eventIdsToRecords.set(rec.eventId, [rec]);
            } else {
                eventRecords.push(rec);
            }
        }

        let data = [];
        let links = []
        let rap = new RecordAndPoints();
        eventIdsToRecords.forEach((events) => {
            if (events.length > 2) {
                // 有些场景下，会同时打印常规的event日志和业务语义的event日志，此时直接丢弃常规event日志，以获取更精确的表达
                events = dropGeneralEventRecord(events);
                if (events.length > 2) {
                    console.log("ERROR: Invalid number of events: ", events);
                    return;
                }
            }

            let sendPoint = {
                name: "send-event-" + events[0].eventId,
                x: this.xCalc.calc(events[0].timestamp),
                y: this.yCacl.calc(parseInt(events[0].stream, 10))
            };
            data.push(rap.addPoint(sendPoint));

            if (events.length === 2) {
                let waitPoint = {
                    name: "wait-event-" + events[1].eventId,
                    x: this.xCalc.calc(events[1].timestamp),
                    y: this.yCacl.calc(parseInt(events[1].stream, 10))
                };
                data.push(rap.addPoint(waitPoint));
                links.push({
                    source: sendPoint.name,
                    target: waitPoint.name,
                    symbol:  ["none", "arrow"],
                    label: {
                        show: true,
                        formatter: () => events[0].type.desc + "-" + events[0].eventId
                    },
                    lineStyle: {
                        color: "#3333CC",
                    }
                });
            }
        });
        return {
            rap: rap,
            series: {
                data: data,
                links: links
            }
        }
    }
}

export class SeriesBuilder {
    constructor() {
        this.rap = new RecordAndPoints();
        this.series = {
            type: 'graph',
            lineStyle: {
                opacity: 0.9,
                width: 5
            },
            data: [],
            links: []
        };
    }

    /**
     *
     * @param seriesInfo: {
     *     rap: RecordAndPoints,
     *     series: {
     *         data: [],
     *         links: []
     *     }
     * }
     */
    mergeSeriesInfo(seriesInfo) {
        this.series.data.push(...seriesInfo.series.data);
        this.series.links.push(...seriesInfo.series.links);
        this.rap.merge(seriesInfo.rap);
    }
    build() {
        return [this.series];
    }
}

export class BlockView {
    constructor(streamNum, xWidth, yHeight) {
        this.streamNum = streamNum;
        this.xWidth = xWidth;
        this.yHeight = yHeight;
        this.records = [];
    }

    addRecord(record) {
        this.records.push(toRaw(record));
    }

    /**
     * ensure birth-recycle and borrow-recycle records has been draw
     * @param rap
     * @param xCalc
     * @param yCalc
     * @returns {{rap: RecordAndPoints, series: {data: *[], links: *[]}}}
     */
    drawGlobalRecyclePoint(rap, xCalc, yCalc) {
        let ret = {
            rap: new RecordAndPoints(),
            series: {
                data: [],
                links: []
            }
        };
        for (let rec of this.records) {
            if (rec.type === RecordType.birthRecycle || rec.type === RecordType.recycle) {
                let points = rap.getPoints(rec);
                if (points.length === 0) {
                    ret.series.data.push(ret.rap.add(rec, {
                        name: rec.type.desc,
                        symbolSize: 15,
                        itemStyle: {
                            color: "#CC3333"
                        },
                        x: xCalc.calc(rec.timestamp),
                        y: yCalc.calc(parseInt(rec.stream, 10))
                    }));
                }
            }
        }
        return ret;
    }

    draw() {
        if (this.records.length === 0) {
            return [];
        }

        let start = this.records[0].timestamp;
        let end = this.records[this.records.length - 1].timestamp;
        let pad = (end - start) / 10;
        start -= pad;
        end += pad;

        let seriesBuilder = new SeriesBuilder();

        // draw lines for streams
        let namer = new NamerForRecordType(this.records);
        let xCalc = new AxisCalculator(this.xWidth, start, end);
        let yCalc = new YAxisCalculator(this.yHeight, this.streamNum);
        for (let streamId = 0; streamId < this.streamNum; ++streamId) {
            let streamLineRender = new StreamLineRender(
                String(streamId),
                start,
                end,
                yCalc.calc(streamId),
                this.xWidth,
                this.records,
                namer);
            seriesBuilder.mergeSeriesInfo(streamLineRender.draw());
        }

        seriesBuilder.mergeSeriesInfo(this.drawGlobalRecyclePoint(seriesBuilder.rap, xCalc, yCalc));

        seriesBuilder.mergeSeriesInfo(new EventsLineRender(this.records, xCalc, yCalc).draw());
        // todo draw launch points
        return seriesBuilder.build();
    }
}